// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © LuxAlgo

//@version=5
indicator("Monte Carlo Shuffled Projection [LuxAlgo]", "LuxAlgo - Monte Carlo Shuffled Projection", overlay = true, max_polylines_count = 100)

//---------------------------------------------------------------------------------------------------------------------}
//Inputs
//---------------------------------------------------------------------------------------------------------------------{

lb = input.int(50, title = "Lookback", maxval = 499, tooltip = "Number of bars to use for calculations.", group = "Simulations")
sim_count = input.int(50, title = "Simulation Count", maxval = 1000, tooltip = "Number of simulations to run.", group = "Simulations")
randomize = input.bool(false, title = "Randomize Direction", tooltip = "Changes the directions half of the randomized bars to produce a more evenly distributed range.", group = "Simulations")
display_sims = input.bool(false, title = "Visualize Simulations", tooltip = "Display Max = 100 Polylines per Script", group = "Simulations")
sim_transp = input.int(70, minval = 0, maxval = 100, title = "Simulation Trasparency", group = "Simulations")

fill_color = input.color(color.new(color.gray,90), title = "Shadow Color", group = "Simulation Range", tooltip = "Fills in-between the Highest and Lowest simulations.")

max_min_tog = input.bool(true, title = "Max/Min Value Lines", group = "Max/Min/Avg Simulation Value Lines")
max_color = input.color(#ff5252, title = "Max | Min Colors",inline = "maxmin", group = "Max/Min/Avg Simulation Value Lines")
min_color = input.color(#089981, title = "",inline = "maxmin", group = "Max/Min/Avg Simulation Value Lines")
avg_tog = input.bool(true, title = "Average Line (Slope)", group = "Max/Min/Avg Simulation Value Lines")
avg_color = input.color(color.rgb(25, 98, 255), title = "Average Color", group = "Max/Min/Avg Simulation Value Lines")

stdev_tog = input.bool(false, title = "Standard Deviations", group = "Standard Deviations")
multi = input.float(2, title = "Multiplier", step = 0.5, minval = 0, group = "Standard Deviations")
dev_color = input.color(color.rgb(25, 98, 255), title = "Deviation Color", group = "Standard Deviations")

//---------------------------------------------------------------------------------------------------------------------}
//UDTs
//---------------------------------------------------------------------------------------------------------------------{
type fa
    array<float> ary

//---------------------------------------------------------------------------------------------------------------------}
//Functions
//---------------------------------------------------------------------------------------------------------------------{
is_odd(_num) => (_num/2) - math.floor(_num/2) > 0

d_places() => 
    decimals = str.contains(str.tostring(syminfo.mintick),".")?str.length(array.get(str.split(str.tostring(syminfo.mintick),"."),1)):0
    Zs = array.new_string(na) 
    for i = 1 to decimals
        Zs.push("0")
    out = str.replace_all(str.replace_all(str.replace_all(str.tostring(Zs),",",""),"]",""),"[","")
    "0." + out
            
random_in_range(seed, min, max) => min + (((1103515245 * seed + 12345) % 2147483647) % ((max+1) - min))

get_col(_num) =>
    color col = na
    for i = 0 to 100
        r = random_in_range(_num*i,0,255)
        g = random_in_range(math.pow(_num*i,2),0,255)
        b = random_in_range(math.pow(_num*i,3),0,255)
        if (r + g + b) > 255
            col := color.rgb(r,g,b,sim_transp)
            break
    col

fy_shuffle(_ary,_seed) =>
    final = array.copy(_ary)
    working = array.copy(_ary)
    for [index,value] in final
        r_index = random_in_range(_seed,0,working.size()-1)
        final.set(index,working.get(r_index))
        working.remove(r_index)
    final

//---------------------------------------------------------------------------------------------------------------------}
//Global Variables
//---------------------------------------------------------------------------------------------------------------------{
var p_lin_ary = array.new<polyline>(na) // Polyline Array, Its the array for Polylines!

var can_ary = array.new_float(na) // Candle Movement Array

//---------------------------------------------------------------------------------------------------------------------}
//General Calcs
//---------------------------------------------------------------------------------------------------------------------{
movement = close-open

can_ary.push(movement)

if can_ary.size() > lb
    can_ary.shift()

//---------------------------------------------------------------------------------------------------------------------}
//Generating Simulations, Compiling Data, & Displaying Visualizations
//---------------------------------------------------------------------------------------------------------------------{

if barstate.islast
    //Local Variables
    full_ary = array.new<fa>(na)
    all_data = array.new<fa>(na)
    outer_points = array.new<chart.point>(1,chart.point.now(close))
    stdevs = array.new_float(na)
    float highest_sim = na 
    float lowest_sim = na

    //Clearing polyline array
    for lin in p_lin_ary
        polyline.delete(lin)
    array.clear(p_lin_ary)

    //Shuffling bar data and sending the array if points to the full array
        //Full array stores each simulation line's "Full" set of values (Left to Right)
    for i = 1 to sim_count
        data = fy_shuffle(can_ary,bar_index*i*sim_count)
        p_ary = array.new<chart.point>(1,chart.point.now(close))
        temp_point_ary = array.new_float(na)
        for [index,value] in data
            add_value = (is_odd(index) and randomize?-value:value)
            point = p_ary.last().price + add_value
            p_ary.push(chart.point.from_index(bar_index+(index+1), point))
            temp_point_ary.push(point)
        full_ary.push(fa.new(temp_point_ary))
        if display_sims
            p_lin_ary.push(polyline.new(p_ary, line_color = get_col(i)))
    
    //Re-arranging the data to be stored in arrays of vertical data
        //"All Data" is all data on that bar of simulations
    if full_ary.size() > 0
        for i = 0 to (full_ary.get(0).ary.size()-1)
            vert_ary = array.new_float(na)
            for e = 0 to sim_count-1
                vert_ary.push(full_ary.get(e).ary.get(i))
            all_data.push(fa.new(vert_ary))
    
    //Now that the simulation data is vertically organized we can find averages and stdevs for each point along the way.
    for [index,data] in all_data
        ary = data.ary
        avg = ary.avg() 
        data_max = ary.max()
        data_min = ary.min()
        stdevs.push(ary.stdev())
        if data_max > highest_sim or na(highest_sim)
            highest_sim := data_max
        if data_min < lowest_sim or na(lowest_sim)
            lowest_sim := data_min
        outer_points.push(chart.point.from_index(bar_index+(index+1), nz(data_max,avg)))
        outer_points.unshift(chart.point.from_index(bar_index+(index+1), nz(data_min,avg)))
    

    avg_dev = stdevs.avg()*multi
    end_point = math.avg(outer_points.first().price,outer_points.last().price)
    
    //Visualizations
    p_lin_ary.push(polyline.new(outer_points, line_color = fill_color, fill_color = fill_color))
    
    if max_min_tog
        line.delete(line.new(bar_index-lb,highest_sim,bar_index+lb, highest_sim, color = max_color, width = 2)[1])
        line.delete(line.new(bar_index-lb,lowest_sim,bar_index+lb, lowest_sim, color = min_color, width = 2)[1])
        label.delete(label.new(bar_index+lb,highest_sim, str.tostring(highest_sim,d_places()), style = label.style_label_left,color = color.rgb(0,0,0,100), textcolor = max_color, tooltip = "Highest Simulated Value")[1])
        label.delete(label.new(bar_index+lb,lowest_sim, str.tostring(lowest_sim,d_places()), style = label.style_label_left,color = color.rgb(0,0,0,100), textcolor = min_color, tooltip = "Lowest Simulated Value")[1])
    if avg_tog
        line.delete(line.new(bar_index,close,bar_index+lb,end_point, color = avg_color, width = 2)[1])
    if stdev_tog
        line.delete(line.new(bar_index,close+avg_dev,bar_index+lb,end_point+avg_dev, color = dev_color)[1])
        line.delete(line.new(bar_index,close-avg_dev,bar_index+lb,end_point-avg_dev, color = dev_color)[1])

//---------------------------------------------------------------------------------------------------------------------}